package com.ben.multiple.util;

import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Font;
import java.awt.GradientPaint;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.AffineTransform;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BandCombineOp;
import java.awt.image.BufferedImage;
import java.awt.image.Kernel;
import java.security.SecureRandom;
import java.util.Random;
import lombok.Data;

/**
 * @author Ben
 * @date 2021/2/11
 */
public final class CaptchaUtil {
    private CaptchaUtil() {
    }

    private final static int IMAGE_WIDTH = 160;
    private final static int IMAGE_HEIGHT = 60;
    private final static int FONT_SIZE = 40;

    @Data
    public static class Model {
        /**
         * 10 - 19
         */
        private final int first;
        /**
         * 0 - 9
         */
        private final int second;
        private final int result;
        /**
         * + 、 -
         */
        private final String operator;

        public Model(int first, int second, int result, String operator) {
            this.first = first;
            this.second = second;
            this.result = result;
            this.operator = operator;
        }

    }

    public static Model generator() {
        SecureRandom firstRandom = new SecureRandom();
        int first = firstRandom.nextInt(10) + 10;
        SecureRandom secondRandom = new SecureRandom();
        int second = secondRandom.nextInt(10);
        SecureRandom operatorRandom = new SecureRandom();
        int operatorFlag = operatorRandom.nextInt(2);
        String operator = operatorFlag == 0 ? "+" : "-";
        int result = operatorFlag == 0 ? (first + second) : (first - second);
        return new Model(first, second, result, operator);
    }

    public static BufferedImage createImage(Model model) {
        // 文字
        String text = model.getFirst() + model.getOperator() + model.getSecond() + "=?";
        // 创建文字图像
        BufferedImage bi = renderWord(text);
        // 扭曲、阴影
        bi = getDistortedImage(bi);
        // 添加背景
        bi = addBackground(bi);
        // 边框
        Graphics2D graphics = bi.createGraphics();
        graphics.setColor(new Color(105, 179, 90));
        graphics.draw(new Line2D.Double(0, 0, 0, IMAGE_HEIGHT));
        graphics.draw(new Line2D.Double(0, 0, IMAGE_WIDTH, 0));
        graphics.draw(new Line2D.Double(0, IMAGE_HEIGHT - 1, IMAGE_WIDTH, IMAGE_HEIGHT - 1));
        graphics.draw(new Line2D.Double(IMAGE_WIDTH - 1, IMAGE_HEIGHT - 1, IMAGE_WIDTH - 1, 0));
        return bi;
    }

    private static BufferedImage renderWord(String word) {
        // 字体、字符间距
        Font font = new Font("Arial", Font.BOLD, FONT_SIZE);
        int charSpace = 4;
        // 创建一个图片对象
        BufferedImage image = new BufferedImage(IMAGE_WIDTH, IMAGE_HEIGHT, BufferedImage.TYPE_INT_ARGB);
        // 为当前的 image 创建一个 画笔
        Graphics2D g2D = image.createGraphics();
        // 指定画笔颜色
        g2D.setColor(Color.BLUE);
        g2D.setFont(font);
        // 图像优化
        RenderingHints hints = new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        hints.add(new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY));
        g2D.setRenderingHints(hints);
        // 测量文本所需的信息容器
        FontRenderContext frc = g2D.getFontRenderContext();
        // 字符
        char[] wordChars = word.toCharArray();
        // 字符宽度
        int[] charWidths = new int[wordChars.length];
        // 字符总宽度
        int widthNeeded = 0;
        for (int i = 0; i < wordChars.length; i++) {
            GlyphVector gv = font.createGlyphVector(frc, new char[]{wordChars[i]});
            charWidths[i] = (int) gv.getVisualBounds().getWidth();
            if (i == 0) {
                widthNeeded = widthNeeded + charWidths[i];
            } else {
                widthNeeded = widthNeeded + charWidths[i] + charSpace;
            }
        }
        // 画笔起始位置
        int startPosX = (IMAGE_WIDTH - widthNeeded) / 2;
        int startPosY = (IMAGE_HEIGHT - FONT_SIZE) / 4 + FONT_SIZE;
        // 画字符
        for (int i = 0; i < wordChars.length; i++) {
            g2D.drawChars(new char[]{wordChars[i]}, 0, 1, startPosX, startPosY);
            startPosX = startPosX + charWidths[i] + charSpace;
        }
        return image;
    }

    private static BufferedImage getDistortedImage(BufferedImage baseImage) {
        BufferedImage distortedImage = new BufferedImage(IMAGE_WIDTH, IMAGE_HEIGHT, BufferedImage.TYPE_INT_ARGB);
        Graphics2D graph = distortedImage.createGraphics();
        // 扭曲
        BufferedImage effectImage = rippleFilter(baseImage);
        // 阴影
        effectImage = shadowFilter(effectImage);
        graph.drawImage(effectImage, 0, 0, null, null);
        graph.dispose();
        return distortedImage;
    }

    private static BufferedImage addBackground(BufferedImage baseImage) {
        // 创建背景图
        BufferedImage imageWithBackground = new BufferedImage(IMAGE_WIDTH, IMAGE_HEIGHT, BufferedImage.TYPE_INT_RGB);
        Graphics2D graph = imageWithBackground.createGraphics();
        // 图片优化
        RenderingHints hints = new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
        hints.add(new RenderingHints(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY));
        hints.add(new RenderingHints(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY));
        hints.add(new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY));
        graph.setRenderingHints(hints);
        // 颜色渐变
        GradientPaint paint = new GradientPaint(0, 0, Color.LIGHT_GRAY, IMAGE_WIDTH, IMAGE_HEIGHT, Color.WHITE);
        graph.setPaint(paint);
        graph.fill(new Rectangle2D.Double(0, 0, IMAGE_WIDTH, IMAGE_HEIGHT));
        // 在背景图上画图
        graph.drawImage(baseImage, 0, 0, null);
        return imageWithBackground;
    }

    private static BufferedImage rippleFilter(BufferedImage src) {
        Rectangle transformedSpace = new Rectangle(0, 0, IMAGE_WIDTH, IMAGE_HEIGHT);
        BufferedImage dst = new BufferedImage(src.getColorModel(),
                src.getColorModel().createCompatibleWritableRaster(transformedSpace.width, transformedSpace.height),
                src.getColorModel().isAlphaPremultiplied(), null);
        int[] inPixels = (int[]) src.getRaster().getDataElements(0, 0, IMAGE_WIDTH, IMAGE_HEIGHT, null);
        int[] outPixels = new int[transformedSpace.width];
        float[] out = new float[2];

        for (int y = 0; y < transformedSpace.height; y++) {
            for (int x = 0; x < transformedSpace.width; x++) {
                transformInverse(transformedSpace.x + x, transformedSpace.y + y, out);
                int srcX = (int) Math.floor(out[0]);
                int srcY = (int) Math.floor(out[1]);
                float xWeight = out[0] - srcX;
                float yWeight = out[1] - srcY;
                int nw, ne, sw, se;
                if (srcX >= 0 && srcX < (IMAGE_WIDTH - 1) && srcY >= 0 && srcY < (IMAGE_HEIGHT - 1)) {
                    // 简单的情况下，所有的角落都在图像中
                    int i = IMAGE_WIDTH * srcY + srcX;
                    nw = inPixels[i];
                    ne = inPixels[i + 1];
                    sw = inPixels[i + IMAGE_WIDTH];
                    se = inPixels[i + IMAGE_WIDTH + 1];
                } else {
                    // 有些角与图像不符
                    nw = getPixel(inPixels, srcX, srcY);
                    ne = getPixel(inPixels, srcX + 1, srcY);
                    sw = getPixel(inPixels, srcX, srcY + 1);
                    se = getPixel(inPixels, srcX + 1, srcY + 1);
                }
                outPixels[x] = bilinearInterpolate(xWeight, yWeight, nw, ne, sw, se);
            }
            dst.getRaster().setDataElements(0, y, transformedSpace.width, 1, outPixels);
        }
        return dst;
    }

    /**
     * 振幅、波长
     */
    private static final float X_AMPLITUDE = 7.6f;
    private static final float Y_AMPLITUDE = new Random().nextFloat() + 1.0f;
    private static final float X_WAVE_LENGTH = new Random().nextInt(7) + 8;
    private static final float Y_WAVE_LENGTH = new Random().nextInt(3) + 2;

    private static void transformInverse(int x, int y, float[] out) {
        float nx = (float) y / X_WAVE_LENGTH;
        float ny = (float) x / Y_WAVE_LENGTH;
        float fx = (float) Math.sin(nx);
        float fy = (float) Math.sin(ny);
        out[0] = x + X_AMPLITUDE * fx;
        out[1] = y + Y_AMPLITUDE * fy;
    }

    private static int getPixel(int[] pixels, int x, int y) {
        if (x < 0 || x >= IMAGE_WIDTH || y < 0 || y >= IMAGE_HEIGHT) {
            return pixels[
                    (clamp(y, IMAGE_HEIGHT - 1) * IMAGE_WIDTH)
                            + clamp(x, IMAGE_WIDTH - 1)];
        }
        return pixels[y * IMAGE_WIDTH + x];
    }

    private static int clamp(int x, int b) {
        return (x < 0) ? 0 : Math.min(x, b);
    }

    private static int bilinearInterpolate(float x, float y, int nw, int ne, int sw, int se) {
        float m0, m1;
        int a0 = (nw >> 24) & 0xff;
        int r0 = (nw >> 16) & 0xff;
        int g0 = (nw >> 8) & 0xff;
        int b0 = nw & 0xff;
        int a1 = (ne >> 24) & 0xff;
        int r1 = (ne >> 16) & 0xff;
        int g1 = (ne >> 8) & 0xff;
        int b1 = ne & 0xff;
        int a2 = (sw >> 24) & 0xff;
        int r2 = (sw >> 16) & 0xff;
        int g2 = (sw >> 8) & 0xff;
        int b2 = sw & 0xff;
        int a3 = (se >> 24) & 0xff;
        int r3 = (se >> 16) & 0xff;
        int g3 = (se >> 8) & 0xff;
        int b3 = se & 0xff;

        float cx = 1.0f - x;
        float cy = 1.0f - y;

        m0 = cx * a0 + x * a1;
        m1 = cx * a2 + x * a3;
        int a = (int) (cy * m0 + y * m1);

        m0 = cx * r0 + x * r1;
        m1 = cx * r2 + x * r3;
        int r = (int) (cy * m0 + y * m1);

        m0 = cx * g0 + x * g1;
        m1 = cx * g2 + x * g3;
        int g = (int) (cy * m0 + y * m1);

        m0 = cx * b0 + x * b1;
        m1 = cx * b2 + x * b3;
        int b = (int) (cy * m0 + y * m1);

        return (a << 24) | (r << 16) | (g << 8) | b;
    }

    /**
     * 阴影的 距离、角度、半径、颜色、透明度
     */
    private static final float SHADOW_DISTANCE = 5;
    private static final float SHADOW_ANGLE = (float) Math.PI * 3 / 2;
    private static final int SHADOW_COLOR = 0xff000000;
    private static final float SHADOW_OPACITY = 1f;

    private static BufferedImage shadowFilter(BufferedImage src) {
        float xOffset = SHADOW_DISTANCE * (float) Math.cos(SHADOW_ANGLE);
        float yOffset = -SHADOW_DISTANCE * (float) Math.sin(SHADOW_ANGLE);
        float shadowR = ((SHADOW_COLOR >> 16) & 0xff) / 255f;
        float shadowG = ((SHADOW_COLOR >> 8) & 0xff) / 255f;
        float shadowB = (SHADOW_COLOR & 0xff) / 255f;
        // 从图像的alpha通道制作一个黑色遮罩
        float[][] extractAlpha = {{0, 0, 0, shadowR}, {0, 0, 0, shadowG}, {0, 0, 0, shadowB}, {0, 0, 0, SHADOW_OPACITY}};
        // 目标图
        BufferedImage dst = new BufferedImage(src.getColorModel(),
                src.getColorModel().createCompatibleWritableRaster(IMAGE_WIDTH, IMAGE_HEIGHT),
                src.getColorModel().isAlphaPremultiplied(), null);
        BufferedImage shadow = new BufferedImage(IMAGE_WIDTH, IMAGE_HEIGHT, BufferedImage.TYPE_INT_ARGB);
        new BandCombineOp(extractAlpha, null).filter(src.getRaster(), shadow.getRaster());
        shadow = gaussianFilter(shadow);
        // 绘图
        Graphics2D g = dst.createGraphics();
        g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, SHADOW_OPACITY));
        g.drawRenderedImage(shadow, AffineTransform.getTranslateInstance(xOffset, yOffset));
        g.setComposite(AlphaComposite.SrcOver);
        g.drawRenderedImage(src, null);
        g.dispose();
        return dst;
    }

    private static BufferedImage gaussianFilter(BufferedImage src) {
        int width = src.getWidth();
        int height = src.getHeight();
        BufferedImage dst = new BufferedImage(src.getColorModel(),
                src.getColorModel().createCompatibleWritableRaster(src.getWidth(), src.getHeight()),
                src.getColorModel().isAlphaPremultiplied(), null);
        int[] inPixels = new int[width * height];
        int[] outPixels = new int[width * height];
        src.getRGB(0, 0, width, height, inPixels, 0, width);
        convolveAndTranspose(inPixels, outPixels, width, height);
        convolveAndTranspose(outPixels, inPixels, height, width);
        dst.setRGB(0, 0, width, height, inPixels, 0, width);
        return dst;
    }

    private static final float SHADOW_RADIUS = 5;
    private static final Kernel KERNEL;

    static {
        int r = (int) Math.ceil(SHADOW_RADIUS);
        int rows = r * 2 + 1;
        float[] matrix = new float[rows];
        float sigma = SHADOW_RADIUS / 3;
        float sigma22 = 2 * sigma * sigma;
        float sigmaPi2 = 2 * (float) Math.PI * sigma;
        float sqrtSigmaPi2 = (float) Math.sqrt(sigmaPi2);
        float radius2 = SHADOW_RADIUS * SHADOW_RADIUS;
        float total = 0;
        int index = 0;
        for (int row = -r; row <= r; row++) {
            float distance = row * row;
            if (distance > radius2) {
                matrix[index] = 0;
            } else {
                matrix[index] = (float) Math.exp(-(distance) / sigma22) / sqrtSigmaPi2;
            }
            total += matrix[index];
            index++;
        }
        for (int i = 0; i < rows; i++) {
            matrix[i] /= total;
        }
        KERNEL = new Kernel(rows, 1, matrix);
    }

    private static void convolveAndTranspose(int[] inPixels, int[] outPixels, int width, int height) {
        float[] matrix = KERNEL.getKernelData(null);
        int cols = KERNEL.getWidth();
        int cols2 = cols / 2;
        for (int y = 0; y < height; y++) {
            int index = y;
            int ioffset = y * width;
            for (int x = 0; x < width; x++) {
                float r = 0, g = 0, b = 0, a = 0;
                for (int col = -cols2; col <= cols2; col++) {
                    float f = matrix[cols2 + col];
                    if (f != 0) {
                        int ix = x + col;
                        if (ix < 0) {
                            ix = 0;
                        } else if (ix >= width) {
                            ix = width - 1;
                        }
                        int rgb = inPixels[ioffset + ix];
                        a += f * ((rgb >> 24) & 0xff);
                        r += f * ((rgb >> 16) & 0xff);
                        g += f * ((rgb >> 8) & 0xff);
                        b += f * (rgb & 0xff);
                    }
                }
                int ia = clamp((int) (a + 0.5));
                int ir = clamp((int) (r + 0.5));
                int ig = clamp((int) (g + 0.5));
                int ib = clamp((int) (b + 0.5));
                outPixels[index] = (ia << 24) | (ir << 16) | (ig << 8) | ib;
                index += height;
            }
        }
    }

    private static int clamp(int c) {
        if (c < 0) {
            return 0;
        }
        return Math.min(c, 255);
    }
}
